Simulating Object Oriented Programming in Visual Basic
Compuserve Text posted by Pete Washburn (73750,3141) January 1994
Here's some of the techniques I've used to emulate an OOP language with QB and VB. I must add a couple of comments however. I'm not saying that Basic is an OOP, but with a few techniques, you can get most of the principles of OOP into Basic. These are just a few of techniques I've used. They may not be the most efficient or optimum methods available, but they seem to be working for me. Improvements are always accepted. Debates about the finer parts of OOP theory are not! I do a fair amount of work with system simulation, so I'll use a segment of one of my projects to demonstrate.
Most OOP's contain a class tree to define the behavior of objects within the program. In this demo case, we are modeling a plumbing system, with pipes, hoses, and valves. The class tree may look as follows:
WaterPart
Pipe
Hoses
Valves
Pumps
We'll look at the WaterPart class. It is defined as a Function as follows:
Function WaterPart (hObj, msg, value)
' all data for objects of this type is contained within the Function,
' within the following instance array
Static WaterPartInstances() As waterPartObject
' count of individual objects of this type
Static WaterPartsCount%
' using the hObj passed, find the object's data in the Instance array
' (there are a lot more sophisticated and efficient ways to do this,
' used as an example only)
For idx = 1 To WaterPartsCount%
If WaterPartsInstances(idx).hObj = hObj Then
Exit For
End If
Next idx
' methods
Select Case msg
Case GET_QUANTITY
WaterPart = WaterPartInstances(idx).quantity
Case SET_QUANTITY
If value <> WaterPartInstances(idx).quantity Then
If WaterPartInstances(idx).diameter = 0 Then
value = 0
End If
WaterPartInstances(idx).quantity = value
End If
WaterPart = WaterPartInstances(idx).quantity
Case GET_DIAMETER
WaterPart = WaterPartInstances(idx).diameter
Case SET_DIAMETER
If value <> WaterPartInstances(idx).diameter Then
WaterPartInstances(idx).diameter = value
WaterPartInstances(idx).area = .7854 * value ^ 2
End If
WaterPart = WaterPartInstances(idx).diameter
Case GET_AREA
WaterPart = WaterPartInstances(idx).area
Case NEW_OBJECT
WaterPartsCount% = WaterPartsCount% + 1
idx = WaterPartsCount%
WaterPartInstances(idx).hObj = hObj
WaterPart = Self(hObj, INIT_OBJECT, value)
Case INIT_OBJECT
WaterPart = Self(hObj, SET_DIAMETER, value)
Case DISPLAY_OBJECT
' code to display the part
Case READ_OBJECT
' code to read info about the part from a file
Case WRITE_OBJECT
' code to write part info to a file
Case PRINT_OBJECT
' code to print the part to a printer
Case INIT_CLASS
ReDim WaterPartInstances(1 To MAX_WATERPARTS) As waterPartObject
Case Else
a = "Object Doesn't Understand Message." + Chr$(10) + Chr$(10)
a = a + "Class: Part" + Chr$(10)
a = a + "Object: " + Str$(hObj) + Chr$(10)
a = a + "Message: " + Str$(msg) + Chr$(10)
a = MsgBox(a, 16, "Message Error")
End Select
End Function
Because VB won't let us define user Types within a Function, the Declarations section of our program describes the Type variable that will contain the object's information.
Type waterPartObject
hObj as Integer
diameter As Single
area As Single
quantity As Integer
End Type
Also, constants used by our VB program must be declared in the Declarations section.
' define messages sent to objects
Global Const NEW_OBJECT = 1
Global Const INIT_OBJECT = 2
Global Const DISPLAY_OBJECT = 3
Global Const READ_OBJECT = 4
Global Const WRITE_OBJECT = 5
Global Const INIT_CLASS = 6
Global Const GET_QUANTITY = 100
Global Const SET_QUANTITY = 101
Global Const GET_DIAMETER = 102
Global Const SET_DIAMETER = 103
Global Const GET_AREA = 104
' define max number of water parts
Global Const MAX_WATERPARTS = 30
A language is considered object oriented if it supports three major features:
1. Encapsulation (or information and implementation hiding)
2. Inheritance.
3. Polymorphism
The Function helps us with Encapsulation. Both the data and the methods that work with the data are contained solely within the Function. The Type we defined contains all of the instance variables of an object. An array is created to hold the instance variables of each object of the class. There are many different ways to find the instance data within the array for a specific object. In this example, we simply do a brute search for the key, hObj within the array.
The data is not globally defined, so the only way to access an object's data is by the methods that are defined within the Function. We work with an object by passing the handle of the object (hObj) to the Function along with the message that we want and any additional we need to provide. To get the area of the particular part, anObj, we would send the following:
area = WaterPart(anObj, GET_AREA, Null)
Similarly, to set a new diameter, we would send the following message:
One difference between this technique with VB and a real OOP is that the number of parameters sent to a Function is constant. In a real OOP, each method would be defined separately, with the specific number of parameters it needed. Because we're wrapping our class and all of its methods within one Function, the number of parameters is fixed. Therefore, each message consist of three parameters, even if you don't need all three. Simply send a Null for any parameter that isn't needed. Likewise, there might be some cases that need more than the three parameters specified. With QB, I had defined the third parameter as a string and encoded all information I needed into that string. The Function then parsed out the info as it needed it. VB is a lot more flexible in this regard. I haven't tried it yet, but by defining the third parameter as being of the Variant type, I believe you can send any type of variable you wish as the third parameter. Perhaps even a user defined Type or array could be sent as the third parameter.
Likewise, QB required that you defined the variable type that the Function returned. As a result, I defined all the class functions as being strings. That way, I could encode any variables being returned from the class function into the string. A VB Function again is more flexible, as it doesn't require you to define the variable type it is, and you can return any type of variable you want back from the function.
All of this allows us the advantages of Encapsulation and information and implementation hiding that OOP's normally provide with VB.
The second major feature of an OOP is inheritance. We can easily model that within VB. Let's define another class, Pipe, that is a descendant of the WaterPart class we've already defined.
' message not handled by this class, pass on to ancestor
Pipe = WaterPart(hObj, msg, value)
End Select
End Function
This has been simplified quite a lot, but basically, the only difference Pipe objects have from the parent class, WaterPart is that Pipes have a length in addition to a diameter. Therefore this class has to process all info in regards to the length of the Pipe. Note the Case Else statement though. If the message hasn't been handled by this class, it is passed on to it's ancestor, which is WaterPart in this case. Note that in the WaterPart class function, the Case Else statement has an error trap as the WaterPart class doesn't have an ancestor class, it is a top level class.
Notice that the Pipe class has a SET_DIAMETER method. This overrides the ancestor WaterPart classes method. Actually, it does call it first and then recalculates the volume within the pipe. This brings up a very significant point, if the class sends a message to itself, it needs to be concerned that any overridden methods in its descendants get a chance to process the message first. This is handled within my OOP techniques by calling a special function, Self. Notice in the WaterPart class, that both NEW_OBJECT and INIT_OBJECT send messages to Self. Here's the code for the Self object.
' route a message to the appropriate class of the object
Function Self (hObj, msg, value)
' find the class of the object by looking it up in the object
' class array
Select Case objectClass%(hObj)
Case WATERPART_CLASS
Self = WaterPart(hObj, msg, value)
Case PIPE_CLASS
Self = Pipe(hObj, msg, value)
End Select
End Function
objectClass%() is an array that contains an entry for each object in the program designating what class the object is. Self() simply redirects the message to the appropriate class for the object specified by hObj. Here are the class designators as listed in the Declarations section of the program.
' class identifiers
Global Const WATERPART_CLASS = 1
Global Const PIPE_CLASS = 2
This gives us most of the late binding features of OOP's. Its not as elegant or automatic as it is in most OOP's, but it does work. Just make sure when you create an object, you list its class in the objectClass%() array. A similar array could also be created to list the ancestor class of the object.
The last major characteristic to be modeled is polymorphism. This means that there might be several messages that are system wide and that all (or most) classes can respond to. In our example, messages such as NEW_OBJECT, INIT_OBJECT, DISPLAY_OBJECT, READ_OBJECT, WRITE_OBJECT, PRINT_OBJECT, and INIT_CLASS are examples of polymorphic messages that most classes implement. The sender of the message doesn't need to know about the class, it simply sends the message. The class handles the details. The Self() function is again used to when sending a message to an object in these cases, as the sender doesn't know even the class of the object that it is sending the message to.
Well, that's most of the techniques I've developed for making QB and VB more like a pure OOP. I'm not proposing these techniques as being the best or only way to program! They are simply the techniques that I have developed to provide most of the features I had with Actor in QB and VB. As I work with them, I am refining them and making them more efficient. But the overhead and code to make it work is more complex than it would be if this was a pure OOP.